package com.qianfeng.iot.auth.service.impl;

//
//                            _ooOoo_  
//                           o8888888o  
//                           88" . "88  
//                           (| -_- |)  
//                            O\ = /O  
//                        ____/`---'\____  
//                      .   ' \\| |// `.  
//                       / \\||| : |||// \  
//                     / _||||| -:- |||||- \  
//                       | | \\\ - /// | |  
//                     | \_| ''\---/'' | |  
//                      \ .-\__ `-` ___/-. /  
//                   ___`. .' /--.--\ `. . __  
//                ."" '< `.___\_<|>_/___.' >'"".  
//               | | : `- \`.;`\ _ /`;.`/ - ` : | |  
//                 \ \ `-. \_ __\ /__ _/ .-` / /  
//         ======`-.____`-.___\_____/___.-`____.-'======  
//                            `=---='  
//  
//         .............................................  
//                  佛祖镇楼                  BUG辟易  
//          佛曰:  
//                  写字楼里写字间，写字间里程序员；  
//                  程序人员写程序，又拿程序换酒钱。  
//                  酒醒只在网上坐，酒醉还来网下眠；  
//                  酒醉酒醒日复日，网上网下年复年。  
//                  但愿老死电脑间，不愿鞠躬老板前；  
//                  奔驰宝马贵者趣，公交自行程序员。  
//                  别人笑我忒疯癫，我笑自己命太贱；  
//  


import cn.hutool.core.lang.UUID;
import com.dc3.common.bean.R;
import com.dc3.common.constant.CacheConstant;
import com.qianfeng.iot.auth.event.BindUserEvent;
import com.qianfeng.iot.auth.pojo.Dc3User;
import com.qianfeng.iot.auth.service.UserService;
import com.qianfeng.iot.openfeign.CacheClient;
import com.qianfeng.iot.openfeign.TenantClient;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.query.Query;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.sql.Date;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;

/**
 * Created by Jackiechan on 2022/7/15 15:04
 *
 * @author Jackiechan
 * @version 1.0
 * @since 1.0
 */
@Service
@Transactional
public class UserServiceImpl implements UserService {
    private CacheClient cacheClient;

    @Autowired
    public void setCacheClient(CacheClient cacheClient) {
        this.cacheClient = cacheClient;
    }

    private ApplicationContext context;


    @Autowired
    public void setContext(ApplicationContext context) {
        this.context = context;
    }

    private TenantClient tenantClient;

    @Autowired
    public void setTenantClient(TenantClient tenantClient) {
        this.tenantClient = tenantClient;
    }

    private R2dbcEntityTemplate r2dbcEntityTemplate;


    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @Autowired
    public void setbCryptPasswordEncoder(BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    @Autowired
    public void setR2dbcEntityTemplate(R2dbcEntityTemplate r2dbcEntityTemplate) {
        this.r2dbcEntityTemplate = r2dbcEntityTemplate;
    }

    @Override
    public Mono<Dc3User> findById(String id) {
        return r2dbcEntityTemplate.selectOne(Query.query(Criteria.where("id").is(id)), Dc3User.class);
    }


    @Override
    public Flux<Dc3User> findByIdIn(List<String> ids) {
        return r2dbcEntityTemplate.select(Dc3User.class).from("dc3_user").matching(Query.query(Criteria.where("id").in(ids))).all();
    }

    @Override
    public Flux<Dc3User> findByName(String name) {
        return r2dbcEntityTemplate.select(Dc3User.class).from("dc3_user").matching(Query.query(Criteria.where("name").is(name))).all();
    }

    @Override
    public Mono<List<String>> findByUserId(String name) {
        return findByName(name).map(Dc3User::getId).collectList();
    }

    @Override
    public Mono<Dc3User> findByNameAndTenantName(String name, String tenantName) {
        return r2dbcEntityTemplate.selectOne(Query.query(Criteria.where("name").is(name).and(Criteria.where("tenant_name").is(tenantName))), Dc3User.class);
    }


    @Override
    public Mono<R<Object>> addUser(Mono<Dc3User> dc3User, String tenantId) {

        return dc3User.flatMap(dc3User1 -> {//dc3User1传递过来的数据
                    //租户的校验,比如租户是不是存在,当前租户下面是不是已经有这些数据了
                    //根据租户的id来查询下面所有的用户,然后在看看这些用户的名字中是不是包含当前的用户名
                    //如果不包含就不存在,包含就存在
                    //我们可以反过来查询: 先根据用户名查询id,如果没有id就可以直接添加,有的话再用这个id和租户的id去查询关联关系,如果没有关联关系就可以插入,有关系就不能插入
                    // TODO 其它的判断,比如说A用户的手机号不用了(A不一定走了),B用户办理了这个手机号来这里上班了,结果分配的时候我们需要校验手机号.发现重复
                    //TODO 如果同租户账号重复怎么办? 如果是开放注册就直接提示用户名存在,让注册的人换个账号,如果是非开放,由管理员添加的,管理员自行加个区别的内容就行了比如后面添加部门名字或者1 2 或者年月日等等
                    return findByUserId(dc3User1.getName())//根据传递过来的用户名查询当前名字的所有的用户的id,因为我们允许相同的用户名,但是同一个租户下不允许
                            .flatMap(ids -> {
                                //根据id和租户的id查询关联表
                                //租户提供一个操作,根据用户的id和租户的id查询数量,然后通过openfeign进行调用,最终实现过滤
                                return tenantClient.getCountByTentIdAndUserIds(ids, tenantId)//根据查询到的用户的id和租户的id进行查询数据,看看在当前租户下是不是有某个id
                                        .filter(r -> ((Integer) r.getData()) == 0)//如果返回的数据是0 就代表不存在这数据,我们就执行添加操作
                                        .map(r -> dc3User1);//只有==0的情况下才会执行,否则就不会过来
                            });
                })
                .flatMap(dc3User1 -> {//如果上面返回了0 才会执行这个地方,才会添加数据
                    //主键
                    //假设我们使用uuid,我们也可以使用别的操作,比如根据租户的id拼接,比如我们的id长度32位,前面16是租户的id,后面的16是用户的id
                    dc3User1.setId(UUID.randomUUID().toString(true));
                    //密码
                    dc3User1.setPassword(bCryptPasswordEncoder.encode(dc3User1.getPassword()));
                    //设置租户的名字
                    //当前执行添加用户操作的人是不是这个租户下面的
                    return r2dbcEntityTemplate.insert(Dc3User.class).into("dc3_user").using(dc3User1);
                })
                .map(dc3User1 -> {
//                    //关联插入到租户和用户的关联表,绑定用户
                    HashMap dc3TenantBindMap = new HashMap<>();
                    dc3TenantBindMap.put("tenantId", tenantId);
                    dc3TenantBindMap.put("userId", dc3User1.getId());
                    dc3TenantBindMap.put("description", "普通用户");
//                    Mono<R<Object>> mono = tenantClient.bindUser2Tenant(dc3TenantBindMap);
//                    return mono.map(r -> dc3User1);
                    //我们将绑定用户视为异步操作,通过事件去绑定
                    context.publishEvent(new BindUserEvent(dc3TenantBindMap));
                    return dc3User1;

                })
                .map(dc3User1 -> R.ok()).defaultIfEmpty(R.fail("添加失败,请检查数据是否重复"));

    }

    @Override
    public Mono<String> login(String name, String password, String tentName) {
        //根据用户名和租户名查询用户数据,然后比较密码
        return findByNameAndTenantName(name, tentName)
                .filter(dc3User -> bCryptPasswordEncoder.matches(password, dc3User.getPassword()))//过滤密码是不是一样
                .map(dc3User -> {
                    //生成jwt数据,返回给用户
                    //签名的方式, hs256
                    //签名前的原始数据, 用户名,租户名,xxxx
                    //密码,密码不能每次都是一样的.因为这样产生的jwt的数据就是固定,所以密码是变化的
                    //我们怎么得到一个变换的密码,并且还能记住,因为后面还需要校验
                    String salt = UUID.randomUUID().toString(true);
                    //我们需要将密码保存起来,保存到redis,key是什么?
                    cacheClient.set(CacheConstant.Entity.TENANT + tentName + CacheConstant.Entity.USER + name + CacheConstant.Suffix.SALT, salt).subscribe();
                    Instant now = Instant.now();//现在的时间
                    Instant exp = now.plusSeconds(2 * 60);//有效期两分钟
                    String jwt = Jwts.builder()
                            .setSubject(name)
                            .claim("tenantName", tentName)
                            .claim("phone", dc3User.getPhone())
                            .claim("email", dc3User.getEmail())
                            .claim("description", dc3User.getDescription())
                            .setExpiration(Date.from(exp))//设置过期时间
                            .setIssuedAt(Date.from(now))//设置开始时间
                            .signWith(SignatureAlgorithm.HS256, salt.getBytes(StandardCharsets.UTF_8))
                            .compact();
                    //TODO 还有一种方式,我们直接将jwt的字符串保存到redis中,并且根据有效期去设置有效期数据,当数据过期后会自动从redis中删除
                    //校验的时候只需要比较传递过来的jwt的数据和redis里面的是不是一样,一样就代表没有篡改过,不一样就代表过期了
                    return jwt;

                });

    }
}
